Skip to content

feat(datasource-active-record): expose has_one :through as a to-one relation#327

Merged
bexchauveto merged 2 commits into
mainfrom
feat/has-one-through-to-one
Jul 3, 2026
Merged

feat(datasource-active-record): expose has_one :through as a to-one relation#327
bexchauveto merged 2 commits into
mainfrom
feat/has-one-through-to-one

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Jul 3, 2026

Copy link
Copy Markdown
Member

Re-opens the work from #325 (has_one :through exposed as a to-one relation) with a corrected commit message: the footer is removed so semantic-release publishes this as a minor (1.35.0) rather than a major.

What it does

Maps a has_one :through association to a OneToOne (HasOne) schema instead of ManyToMany, so it renders as a to-one relation (matching forest_liana v1). ActiveRecord resolves the join natively via the association name, so read, filter and aggregate all work through the relation. When every hop of the through chain is a belongs_to (no scope, same database), it folds into the existing to-one LEFT JOIN instead of per-hop preload queries; otherwise it falls back to preload with the intermediate FK selected.

Note

Expose non-polymorphic has_one :through associations as OneToOne relations in ActiveRecord datasource

  • Non-polymorphic has_one :through associations now produce a OneToOneSchema instead of ManyToManySchema in the collection schema builder, with origin_key set to the target model's primary key.
  • When the through chain is composed entirely of belongs_to hops, Utils::Query collapses it into a single SQL query using LEFT OUTER JOINs; other chains fall back to preload.
  • New helpers belongs_to_chain_through?, through_tables, and root_through_foreign_key guard against unsafe JOINs and ensure intermediate foreign key columns are selected.
  • Behavioral Change: Any existing has_one :through (non-polymorphic) relation that previously appeared as ManyToMany in Forest Admin will now appear as OneToOne, which may affect how it is queried or displayed.

Macroscope summarized 0ee19cd.

…elation (#325)

* feat(datasource-active-record): expose has_one :through as a to-one relation

Map a `has_one :through` association to a OneToOne (HasOne) schema instead of
ManyToMany, so it renders as a to-one relation (matching forest_liana v1)
rather than a to-many list. ActiveRecord resolves the join natively via the
association name, so read, filter and aggregate all work through the relation.

Resolve it efficiently too: when every hop of the through chain is a belongs_to
(no scope/default_scope, same database), fold it into the existing to-one LEFT
JOIN instead of issuing per-hop preload queries. When it does fall back to
preload (e.g. the same relation is also used by a filter), the intermediate
foreign key is now selected so ActiveRecord can still load it.

* fix(datasource-active-record): address review feedback on has_one :through to-one

- select the intermediate through FK only when it lives on the root table (the
  through hop is a belongs_to); a has_one through hop keeps the FK on the child,
  so selecting it against the root table emitted an invalid column.
- count the intermediate through table in the JOIN safeguard so a separately
  projected relation on that same table falls back to preload instead of being
  JOINed twice (which ActiveRecord would alias, breaking column references).
- document that a has_one :through to-one is display-only: it has no direct
  foreign key, so updating/associating it via the OneToOne write path is
  unsupported (the link lives on the intermediate table).
- simplify belongs_to_chain_through?.

* docs(datasource-active-record): drop comments added for has_one :through work

* test(datasource-active-record): guard single JOIN for filter-joined has_one :through

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jul 3, 2026

Copy link
Copy Markdown

4 new issues

Tool Category Rule Count
qlty Structure Deeply nested control flow (level = 4) 1
qlty Duplication Found 15 lines of similar code in 2 locations (mass = 94) 1
qlty Structure Function with many returns (count = 8): joinable_target 1
qlty Structure Complex binary expression 1

origin_key: association.klass.primary_key,
origin_key_target: @model.primary_key
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deeply nested control flow (level = 4) [qlty:nested-control-flow]

origin_type_value: is_polymorphic ? @model.name : nil,
foreign_type_field: source_polymorphic ? association.source_reflection.foreign_type : nil,
foreign_type_value: source_polymorphic ? association.options[:source_type] : nil
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 15 lines of similar code in 2 locations (mass = 94) [qlty:similar-code]

return if used_tables.include?(target.model.table_name) # a table joined twice would be aliased by AR
return if through_tables(collection, relation_name).intersect?(used_tables)

target

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many returns (count = 8): joinable_target [qlty:return-statements]

through = reflection.through_reflection
source = reflection.source_reflection

through && source && through.belongs_to? && source.belongs_to? &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complex binary expression [qlty:boolean-logic]

…ugh key

root_through_foreign_key returns an array for a composite-key belongs_to
through hop; interpolating it directly produced malformed SQL. Emit one
select entry per key instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bexchauveto bexchauveto merged commit 9788b60 into main Jul 3, 2026
48 checks passed
@bexchauveto bexchauveto deleted the feat/has-one-through-to-one branch July 3, 2026 09:14
forest-bot added a commit that referenced this pull request Jul 3, 2026
# [1.35.0](v1.34.2...v1.35.0) (2026-07-03)

### Features

* **datasource-active-record:** expose has_one :through as a to-one relation ([#327](#327)) ([9788b60](9788b60))
@forest-bot

Copy link
Copy Markdown
Member

🎉 This PR is included in version 1.35.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants